/**
 * 
 */
package com.tcl.wip.client.repository;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.dbutils.BeanProcessor;
import org.apache.commons.dbutils.DbUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.druid.sql.PagerUtils;
import com.alibaba.druid.util.JdbcUtils;
import com.google.common.base.CaseFormat;
import com.google.common.base.Joiner;
import com.tcl.wip.client.annotation.CreatedTime;
import com.tcl.wip.client.annotation.ModifiedTime;
import com.tcl.wip.client.exception.WipException;
import com.tcl.wip.client.orm.IdGenerator;
import com.tcl.wip.client.orm.NameConvertor;
import com.tcl.wip.client.orm.WipBeanProcessor;
import com.tcl.wip.client.query.Column;
import com.tcl.wip.client.query.Parameter;
import com.tcl.wip.client.query.Query;
import com.tcl.wip.client.query.QueryBuilder;
import com.tcl.wip.client.query.SimpleExpression;

/**
 * @author zhaowen.zhuang
 * @Date Jan 19, 2015
 */
public class SimpleWipRepository<E, ID, SK> implements WipRepository<E, ID, SK> {


    private static final Logger log = LoggerFactory.getLogger(SimpleWipRepository.class);

    private static final Map<Class<?>, Integer> DefaultJdbcTypeMap = createTypeMap();

    private static Map<Class<?>, Integer> createTypeMap() {
        Map<Class<?>, Integer> map = new HashMap<>();
        map.put(Integer.class, Types.INTEGER);
        map.put(Long.class, Types.BIGINT);
        map.put(String.class, Types.VARCHAR);
        map.put(Date.class, Types.TIMESTAMP);
        map.put(Enum.class, Types.VARCHAR);
        return Collections.unmodifiableMap(map);
    }

    private EntityMeta meta;
    private DatasourceManager datasourceManager;


    public SimpleWipRepository(EntityMeta meta, DatasourceManager datasourceManager) {
        this.meta = meta;
        this.datasourceManager = datasourceManager;

    }


    @SuppressWarnings({"unchecked", "rawtypes"})
    @Override
    public boolean insert(E entity) {
        IdGenerator<?> idGenerator = meta.getIdGenerator();
        if (idGenerator != null) {
            Object id = idGenerator.generate();
            setFieldValue(meta.getIdField(), entity, id);
        }

        SK shardingKey = (SK) getFieldValue(meta.getShardingKeyField(), entity);
        if (shardingKey == null) {
            throw new WipException("shard key can not be null when insert!");
        }

        List<Field> fields = meta.getPropertyFields();

        List<String> columns = new ArrayList<>(fields.size() + 2);
        // TODO 更精确的 jdbcType
        List<Parameter<?>> parameters = new ArrayList<>(fields.size() + 2);
        List<String> valueExps = new ArrayList<>(columns.size());
        
        columns.add(NameConvertor.toColumnName(meta.getIdField().getName()));
        parameters.add(new Parameter(getFieldValue(meta.getIdField(), entity), DefaultJdbcTypeMap
                .get(meta.getIdField().getType())));
        valueExps.add("?");
        
        if (meta.getIdField() != meta.getShardingKeyField()) {
            columns.add(NameConvertor.toColumnName(meta.getShardingKeyField().getName()));
            parameters.add(new Parameter(shardingKey, DefaultJdbcTypeMap.get(meta
                    .getShardingKeyField().getType())));
            valueExps.add("?");
        }
        for (Field field : fields) {

            if (Date.class.isAssignableFrom(field.getType())
                    && (field.isAnnotationPresent(CreatedTime.class) || field
                            .isAnnotationPresent(ModifiedTime.class))) {
                setFieldValue(field, entity, new Date());
                valueExps.add("now()");
            } else {
                parameters.add(new Parameter(getFieldValue(field, entity), DefaultJdbcTypeMap
                        .get(field.getType())));
                valueExps.add("?");
            }

            columns.add(NameConvertor.toColumnName(field.getName()));

        }

        StringBuilder sql = new StringBuilder("insert into ");
        sql.append(meta.getTableName()).append("(").append(Joiner.on(",").join(columns))
                .append(") values(");
        sql.append(Joiner.on(",").join(valueExps));
        sql.append(")");


        return execute(shardingKey, sql.toString(), parameters, new StatementExecutor<Boolean>() {

            @Override
            public Boolean execute(PreparedStatement statement) throws SQLException {
                try {
                    return statement.executeUpdate() > 0;
                } finally {
                    DbUtils.close(statement);
                }
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean update(E entity) {
        SK shardingKey = (SK) getFieldValue(meta.getShardingKeyField(), entity);
        if (shardingKey == null) {
            throw new WipException("shard key can not be null when update!");
        }

        List<Field> fields = meta.getPropertyFields();

        List<String> settingExps = new ArrayList<>(fields.size() + 2);
        // TODO 更精确的 jdbcType
        List<Parameter<?>> parameters = new ArrayList<>(fields.size() + 2);

        StringBuilder sql = new StringBuilder("update ");
        sql.append(meta.getTableName()).append(" set ");
        for (Field field : fields) {

            if (Date.class.isAssignableFrom(field.getType())
                    && field.isAnnotationPresent(ModifiedTime.class)) {
                setFieldValue(field, entity, new Date());
                settingExps.add(NameConvertor.toColumnName(field.getName()) + " = now()");
            } else {
                settingExps.add(NameConvertor.toColumnName(field.getName()) + " = ?");
                parameters.add(new Parameter<Object>(getFieldValue(field, entity),
                        DefaultJdbcTypeMap.get(field.getType())));
            }


        }
        sql.append(Joiner.on(",").join(settingExps));

        sql.append(" where ")
                .append(NameConvertor.toColumnName(meta.getShardingKeyField().getName()))
                .append(" = ? ");

        parameters.add(new Parameter<Object>(shardingKey, DefaultJdbcTypeMap.get(meta
                .getShardingKeyField().getType())));
        if (meta.getShardingKeyField() != meta.getIdField()) {
            sql.append(" and ").append(NameConvertor.toColumnName(meta.getIdField().getName()))
                    .append(" = ? ");
            parameters.add(new Parameter<Object>(getFieldValue(meta.getIdField(), entity),
                    DefaultJdbcTypeMap.get(meta.getIdField().getType())));
        }

        return execute(shardingKey, sql.toString(), parameters, new StatementExecutor<Boolean>() {

            @Override
            public Boolean execute(PreparedStatement statement) throws SQLException {
                try {
                    return statement.executeUpdate() == 1;
                } finally {
                    DbUtils.close(statement);
                }
            }
        });
    }

    @Override
    public boolean delete(ID id, SK shardKey) {
        if (id == null || shardKey == null) {
            return false;
        }
        QueryBuilder<E, SK> queryBuilder = withShardingKey(shardKey);
        if (meta.getIdField() != meta.getShardingKeyField()) {
            queryBuilder
                    .and(new SimpleExpression(NameConvertor.toColumnName(meta.getIdField()
                            .getName()), "%s = ?", new Parameter(id, DefaultJdbcTypeMap.get(id
                            .getClass()))));
        }
        return queryBuilder.delete() == 1;

    }

    @Override
    public QueryBuilder<E, SK> withShardingKey(SK sk) {
        QueryBuilder<E, SK> builder = new QueryBuilder<E, SK>(this);
        builder.withShardingKey(
                new Column(NameConvertor.toColumnName(meta.getShardingKeyField().getName()), meta
                        .getShardingKeyField().getType(), DefaultJdbcTypeMap.get(meta
                        .getShardingKeyField().getType())), sk);
        return builder;
    }

    public List<E> list(Query<SK> query) {
        String sql = "select * from " + meta.getTableName() + query.getWhere();
        return executeSelect(query, sql);
    }

    /**
     * @param query
     * @param limit
     * @param offset
     * @return
     */
    public List<E> listPage(Query<SK> query, int offset, int limit) {
        String sql = "select * from " + meta.getTableName() + query.getWhere();
        sql = PagerUtils.limit(sql, JdbcUtils.MYSQL, offset, limit);
        return executeSelect(query, sql);
    }


    /**
     * @param query
     * @param sql
     * @return
     */
    private List<E> executeSelect(Query<SK> query, String sql) {
        return execute(query.getShardingKey(), sql, query.getParameters(),
                new StatementExecutor<List<E>>() {

                    @Override
                    public List<E> execute(PreparedStatement statement) throws SQLException {
                        statement.execute();
                        ResultSet resultSet = statement.getResultSet();
                        BeanProcessor processor =
                                new WipBeanProcessor(meta.getColumnToPropertyMap());
                        try {
                            List<?> list = processor.toBeanList(resultSet, meta.getEntityType());
                            return (List<E>) list;
                        } finally {
                            DbUtils.close(resultSet);
                        }
                    }
                });
    }


    public int delete(Query<SK> query) {
        String sql = "delete from " + meta.getTableName() + query.getWhere();
        return execute(query.getShardingKey(), sql, query.getParameters(),
                new StatementExecutor<Integer>() {

                    @Override
                    public Integer execute(PreparedStatement statement) throws SQLException {
                        try {
                            return statement.executeUpdate();
                        } finally {
                            DbUtils.close(statement);
                        }
                    }


                });
    }


    public int count(Query<SK> query) {
        String sql = "select count(*) from " + meta.getTableName() + query.getWhere();
        return execute(query.getShardingKey(), sql, query.getParameters(),
                new StatementExecutor<Integer>() {

                    @Override
                    public Integer execute(PreparedStatement statement) throws SQLException {
                        try {
                            ResultSet resultSet = statement.executeQuery();
                            resultSet.next();
                            return resultSet.getInt(1);
                        } finally {
                            DbUtils.close(statement);
                        }
                    }


                });
    }

    @SuppressWarnings("unchecked")
    private <T> T execute(SK shardingKey, String prepareSql, List<Parameter<?>> parameters, StatementExecutor executor) {
        if (log.isDebugEnabled()) {
            log.debug("running sql: [{}] with parameters {}", prepareSql, parameters);
        }

        Connection connection = null;
        try {
            connection = datasourceManager.getDatasource(String.valueOf(shardingKey)).getConnection();
            PreparedStatement statement = connection.prepareStatement(prepareSql);
            for (int i = 0; i < parameters.size(); i++) {
                Parameter<?> parameter = parameters.get(i);
                statement.setObject(i + 1, parameter.getValue(), parameter.getJdbcType());
            }

            return (T) executor.execute(statement);
        } catch (SQLException e) {
            throw new WipException(e);
        } finally {
            if (connection != null) {
                try {
                    DbUtils.close(connection);
                } catch (SQLException e) {
                    log.warn("error occurred while close connection.", e);
                }
            }
        }
    }


    private Map<String, String> createColumnToPropertyMap(Class<?> entityType) {
        Map<String, String> map = new HashMap<String, String>();
        Field[] fields = entityType.getDeclaredFields();
        for (Field field : fields) {
            if ((field.getModifiers() & Modifier.STATIC) != Modifier.STATIC) {
                map.put(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field.getName()),
                        field.getName());
            }

        }
        return map;
    }


    interface StatementExecutor<T> {
        T execute(PreparedStatement statement) throws SQLException;
    }


    public Object getFieldValue(Field field, Object target) {
        try {
            return field.get(target);
        } catch (IllegalAccessException e) {
            throw new WipException(e);
        }
    }


    public void setFieldValue(Field field, Object target, Object value) {
        try {
            field.set(target, value);
        } catch (IllegalAccessException e) {
            throw new WipException(e);
        }
    }

}
